Node.jsでコネクションプールを使ってMySQLの暗黙的コミットを起こしてみた
こんにちは、ゲームソリューション部のsoraです。
今回は、コネクションプールを使ってMySQLの暗黙的コミットを起こしてみたことについて書いていきます。
MySQLの暗黙的コミットとは
公式ドキュメントは以下です。
暗黙的コミットについてまとめたブログは以下です。
暗黙的コミットの中で、今回関係のある部分は以下です。
- トランザクション制御のステートメント
- BEGIN、LOCK TABLES、SET autocommit = 1 (1でない状態から1に変えた場合)、START TRANSACTION、UNLOCK TABLESなどでは暗黙的コミットが入る
- トランザクションのネストはできない
今回は、実際にAPIサーバを構築して、コネクションプールを使って暗黙的コミットを起こしてみます。
本ブログでは意図的にやっていますが、実際の開発時にもトランザクション処理でコミットやロールバックを忘れると、意図しない暗黙的コミットが発生してしまいます。
環境構築
ECS(Fargate)でAPIサーバ(Node.js)を構築し、Amazon Aurora(MySQL)へクエリを実行する構成にします。
APIサーバ
以下がAPIサーバのコードです。(言語はTypeScript)
意図的に暗黙的コミットを起こすため、コネクションプールを1にして2回のリクエストが同じコネクションを使用するようにし、try文の中でawait connection.commit();
をあえて書き忘れた形にしています。
今回の検証に関係はありませんが、ロガーとしてpinoを使用してCloudWatch Logsにログを出力しています。
コネクションプールを使うパターンと使わないパターンの両方で実施するため、コメントアウトでコネクションプールを使わないパターンも記載しています。
import express, { Request, Response } from 'express';
import mysql, { Pool, PoolConnection } from 'mysql2/promise';
// import mysql, { Connection } from 'mysql2/promise';
import 'dotenv/config';
import pino from 'pino';
// 標準出力
const logger = pino();
const app = express();
// データベース接続設定
const dbConfig = {
host: process.env.HOST as string,
port: parseInt(process.env.PORT as string, 10),
user: process.env.USER as string,
password: process.env.PASSWORD as string,
database: process.env.DATABASE as string,
connectionLimit: 1
};
// コネクションプールの作成
const pool: Pool = mysql.createPool(dbConfig);
// リクエストを受けるポート
const PORT = 3000;
// テストAPI
app.get('/implicit-test', async (req, res) => {
let connection: PoolConnection | undefined;
// let connection: Connection | undefined;
const {prefecture, prefectural_capital} = req.query;
try {
connection = await pool.getConnection();
// connection = await mysql.createConnection(dbConfig);
logger.info('Pool get successfully');
await connection.beginTransaction();
logger.info('Transaction started');
await connection.execute(
`INSERT INTO prefectures (
prefecture,
prefectural_capital
) VALUES (?, ?)`,
[prefecture, prefectural_capital]
);
// トランザクションの結果を返却
res.status(200).json({
message: 'Query OK',
prefecture,
prefectural_capital
});
} catch (err: any) {
if (connection) {
await connection.rollback();
}
res.status(500).json({ error: 'Database query failed' });
logger.error({
msg: 'Error executing query',
errMessage: err.message,
errStack: err.stack
});
} finally {
if (connection) {
connection.release();
// connection.end();
}
}
});
app.use((req: Request, res: Response) => {
logger.error('Not Found:', req.originalUrl);
res.status(404).json({ message: 'Not Found' });
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
ECS上で実行するため、dockerfileも作成します。
その他、package.json
や.env
ファイルも必要ですが記載は割愛します。
FROM node:alpine
WORKDIR /app
RUN apk update
COPY /ts .
RUN npm install
CMD ["npm", "run", "start"]
DB
DBには、事前にデータベースとテーブルを作成しておきます。
検証用にMySQLクライアントがインストールされているEC2インスタンスがあったため、そちらを使用して事前準備をします。
CREATE DATABASE test;
USE test;
CREATE TABLE prefectures (
id SERIAL PRIMARY KEY,
prefecture VARCHAR(255) NOT NULL,
prefectural_capital VARCHAR(255) NOT NULL
);
動作検証
検証の流れとして、まず1つ目のリクエストを実行した後、テーブル内のデータを確認します。
その後、2つ目のリクエストを実行した後に、同様にテーブル内のデータを確認します。
コネクションプールを使ったパターン
1つ目のリクエストを実行します。
http://{public ip}:3000/implicit-test?prefecture=Kanagawa&prefectural_capital=Yokohama
{"message":"Query OK","prefecture":"Kanagawa","prefectural_capital":"Yokohama"}
テーブル内のデータを確認してみると、トランザクション処理でコミットしていないため、データは挿入されていません。
mysql> select * from prefectures;
Empty set (0.00 sec)
次に2つ目のリクエストを実行します。
今回、コネクションプールは1にしているため、1つ目のリクエストと同じコネクションが使われることになります。
http://{public ip}:3000/implicit-test?prefecture=Hyogo&prefectural_capital=Kobe
{"message":"Query OK","prefecture":"Hyogo","prefectural_capital":"Kobe"}
テーブル内のデータを確認してみると、await connection.beginTransaction();
で暗黙的コミットが発生し、1つ目のリクエストのデータがコミットされて挿入されています。
2つ目のリクエストのデータはコミットしていないため入っていません。
mysql> select * from prefectures;
+----+------------+---------------------+
| id | prefecture | prefectural_capital |
+----+------------+---------------------+
| 1 | Kanagawa | Yokohama |
+----+------------+---------------------+
1 row in set (0.00 sec)
コネクションプールを使わないパターン(都度コネクションを作成)
先ほどと同様に進めていきます。
まず1つ目のリクエストを実行します。
http://{public ip}:3000/implicit-test?prefecture=Kanagawa&prefectural_capital=Yokohama
{"message":"Query OK","prefecture":"Kanagawa","prefectural_capital":"Yokohama"}
テーブル内のデータを確認してみると、トランザクション処理でコミットしていないため、データは挿入されていません。
mysql> select * from prefectures;
Empty set (0.00 sec)
次に2つ目のリクエストを実行します。
コネクションプールを使用しておらず、都度コネクションを作成するため、1つ目のリクエストと使用するコネクションは異なります。
http://{public ip}:3000/implicit-test?prefecture=Hyogo&prefectural_capital=Kobe
{"message":"Query OK","prefecture":"Hyogo","prefectural_capital":"Kobe"}
テーブル内のデータを確認してみると、コネクションが別のため暗黙的コミットは発生していません。
mysql> select * from prefectures;
Empty set (0.00 sec)
最後に
今回は、コネクションプールを使ってMySQLの暗黙的コミットを起こしてみたことを記事にしました。
MySQLでトランザクション処理をする際は、コミットとロールバックの記載を忘れないようにしないと、意図せずコミットが入ってしまうため注意しましょう。
私はこれほど単純なコードではないものの、上記のような事象が発生したため、知見を残すためにブログにしました。
どなたかの参考になると幸いです。